/* Copyright 2013 (C) FatalEror / Dean Bahari
 * 
 * jroCK is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * See LICENSE file in main folder for more information.
 */

package jrock.filesystem;

import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.CharBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;

/**
 * @author FatalEror
 */
public class GRF {
    
    private final String magicKey    = "Master of Magic\0";
    private int          version     = 0;
    private int          tableOffset = 0;
    private int          fileCount   = 0;
    private Item[]       item        = null;
    private FileChannel  fc          = null;
    
    public class Item {
        private GRF    parent          = null;
        private String fileName        = "";
        private int    lenCompressed   = 0;
        private int    lenCompressedA  = 0;     // For encryption
        private int    lenUncompressed = 0;
        private byte   flag            = 0;
        private int    offset          = 0;
        
        public ByteBuffer getData() {
            return parent.getData(this);
        }
        public String getName() {
            return fileName;
        }
        public int getSizePack() {
            return lenCompressed;
        }
        public int getSizeReal() {
            return lenUncompressed;
        }
    }
    
    public boolean open(String fileName) {
        try {
            fc = new FileInputStream(fileName).getChannel();
            
            // Read GRF Header (first 46 bytes)
            MappedByteBuffer mb = fc.map(FileChannel.MapMode.READ_ONLY, 0, 46);
            mb.order(ByteOrder.LITTLE_ENDIAN);
            
            // Check if input file is a valid GRF by its signature/magic key
            byte[] tmpData = new byte[magicKey.length()];
            mb.get(tmpData, 0, magicKey.length());
            if (!magicKey.contentEquals(new String(tmpData))) {
                throw new Exception("Error: " + fileName + " is not a valid GRF.");
            }
            
            // GRF Encryption, rarely used in official GRF
            mb.position(mb.position() + 14);
            
            tableOffset = mb.getInt() + 46;
            fileCount   = -mb.getInt() + mb.getInt() - 7;
            version     = mb.getInt();
            
            // Read table data, 10MB (0xA00000) should be enough to hold entire
            // compressed table data
            mb = fc.map(FileChannel.MapMode.READ_ONLY, tableOffset, tableOffset + 0xA00000 > fc.size() ? fc.size() - tableOffset : 0xA00000);
            mb.order(ByteOrder.LITTLE_ENDIAN);
            
            byte[] bufCompressed   = new byte[mb.getInt()];
            byte[] bufUncompressed = new byte[mb.getInt()];
            
            mb.get(bufCompressed, 0, bufCompressed.length);
            
            // Decompress buffer data
            Inflater inf = new Inflater();
            inf.setInput(bufCompressed);
            inf.inflate(bufUncompressed);
            
            ByteBuffer bb = ByteBuffer.wrap(bufUncompressed);
            bb.order(ByteOrder.LITTLE_ENDIAN);
            item = new Item[fileCount];
            
            // Length of filename inside GRF should not be longer than 256
            CharBuffer tmpName = CharBuffer.allocate(256);
            byte       tmpByte;
            
            // Loop through table data
            for (int i = 0; i < fileCount; i++) {
                while ((tmpByte = bb.get()) != 0) {
                    tmpName.put((char) (((char)tmpByte) & 0xFF));
                }
                tmpName.flip();
               
                item[i] = new Item();
                item[i].parent          = this;
                item[i].fileName        = tmpName.toString();
                item[i].lenCompressed   = bb.getInt();
                item[i].lenCompressedA  = bb.getInt();
                item[i].lenUncompressed = bb.getInt();
                item[i].flag            = bb.get();
                item[i].offset          = bb.getInt() + 46;
                
                tmpName.clear();
            }
            
        } catch (IOException ex) {
            Logger.getLogger(GRF.class.getName()).log(Level.SEVERE, null, ex);
        } catch (Exception ex) {
            Logger.getLogger(GRF.class.getName()).log(Level.SEVERE, null, ex);
            this.close();
        }
        
        return true;
    }
    
    public ByteBuffer getData(String fileName) throws Exception {
        return this.getData(this.getItem(fileName));
    }
    
    public ByteBuffer getData(Item item) {
        try {
            MappedByteBuffer mb = fc.map(FileChannel.MapMode.READ_ONLY, item.offset, item.lenCompressed);
            
            byte[] bufCompressed   = new byte[item.lenCompressed];
            byte[] bufUncompressed = new byte[item.lenUncompressed];
            
            mb.get(bufCompressed, 0, item.lenCompressed);
            
            Inflater inf = new Inflater();
            inf.setInput(bufCompressed);
            inf.inflate(bufUncompressed);
            
            return ByteBuffer.wrap(bufUncompressed);
        } catch (IOException | DataFormatException ex) {
            Logger.getLogger(GRF.class.getName()).log(Level.SEVERE, null, ex);
        }
        
        return null;
    }
    
    public Item getItem(String fileName) throws Exception {
        for (int i = 0; i < fileCount; i++) {
            if (fileName.equalsIgnoreCase(item[i].fileName)) {
                return item[i];
            }
        }
        throw new Exception("Error: Can't find " + fileName + ".");
    }
    
    public int getCount() {
        return fileCount;
    }
    
    public int getVersion() {
        return version;
    }
    
    public GND getGND(String fileName) {
        GND gnd = new GND();
        gnd.read(this, fileName);
        return gnd;
    }
    
    public RSM getRSM(String fileName) {
        RSM rsm = new RSM();
        rsm.read(this, fileName);
        return rsm;
    }
    
    public RSW getRSW(String fileName) {
        RSW rsw = new RSW();
        rsw.read(this, fileName);
        return rsw;
    }
    
    public BMP getBMP(String fileName) {
        BMP bmp = new BMP();
        bmp.read(this, fileName);
        return bmp;
    }
    
    public void close() {
        if (fc.isOpen()) {
            try {
                fc.close();
            } catch (IOException ex) {
                Logger.getLogger(GRF.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }
}
